Error Handling for REST

Spring에는 여러가지 Exception Handling 방식이 있다. 하지만 REST API에서는 또 다른 방식의 예외 처리 방식이 있는데 그걸 알아보려고 한다.

간략한 역사

  • Spring 3.2 이전: Spring MVC Application에서는 HandlerExceptionResolver@ExceptionHandler Annotation을 이용하는 것이 대표적인 방법이였다. 하지만 단점도 존재했다.
  • Spring 3.2 이후: @ControllerAdvice가 등장하였고, 위의 두 방법의 단점을 해결하였고, 애플리케이션 전체에서 통일된 예외처리가 가능해졌다.
  • Spring 5: REST API를 위한 ResponseStatusException 클래스가 등장하였다.

1번째: @ExceptionHandler

Exception Handler에서 설명했듯이, @ExceptionHandler@Controller 가 붙은 컨트롤러의 메서드에 붙여 예외를 처리하는 것이다.

다만 해당 컨트롤러에서만 예외처리를 할 수 있다.

@Controller
public class ExceptionController{
	@ExceptionHandler({IllegalArgumentException.class})  
	public ResponseEntity<String> handle(final Exception exception) {  
		return ResponseEntity.badRequest().body(exception.getMessage());  
	}
}

여기서 handle은 오로지 ExceptionController 범위 안에서만 IllegalArgumentException을 잡아낼 수 있다.

만약 @ControllerAdvice 어노테이션이 붙은 클래스에서 사용하면 전역적으로 예외 처리가 가능하다.

2번째: ResponseStatus를 사용한 예외 처리

Custom Exception에 @ResponseStatus를 붙이거나 ResponseStatusException 예외를 발생시켜서 예외처리가 가능하다.

Custom Exception에 @ResponseStatus 붙여주기

DispactherServlet에는 ResponseStatusExceptionResolver가 기본으로 활성화되어있는데, 이 리졸버가 처리한다.

https://www.baeldung.com/spring-response-status#controller

When we want to signal an error, we can provide an error message via the reason argument:

@ResponseStatus(HttpStatus.BAD_REQUEST, reason = "Some parameters are invalid")
void onIllegalArgumentException(IllegalArgumentException exception) {}

Note, that when we set reason, Spring calls HttpServletResponse.sendError(). Therefore, it will send an HTML error page to the client, which makes it a bad fit for REST endpoints. Also note, that Spring only uses @ResponseStatus, when the marked method completes successfully (without throwing an Exception).

TL;DR: ResponseStatusHttpServletResponse.sendError()를 호출하기 때문에 REST endpoint에는 맞지 않는다.

ResponseStatusException 예외 발생시키기

https://www.baeldung.com/spring-response-status-exception

Spring 5 이후에 등장한 예외로, REST하게 예외를 발생시킬 수 있도록 한다. @ResponseStatus와 마찬가지로 ResponseStatusExceptionResolver가 처리한다.

3가지의 생성자가 있다:

ResponseStatusException(HttpStatus status)
ResponseStatusException(HttpStatus status, java.lang.String reason)
ResponseStatusException(
  HttpStatus status, 
  java.lang.String reason, 
  java.lang.Throwable cause
)
  • status: HTTP Status
  • reason: 예외 메세지
  • cause: 예외가 발생된 원인 예외

사용 예시는 아래와 같다.

@GetMapping("/actor/{id}")
public String getActorName(@PathVariable("id") int id) {
    try {
        return actorService.getActor(id);
    } catch (ActorNotFoundException ex) {
        throw new ResponseStatusException(
          HttpStatus.NOT_FOUND, "Actor Not Found", ex);
    }
}

응답은 아래와 같이 온다.

$ curl -i -s -X GET http://localhost:8081/actor/8
HTTP/1.1 404
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 26 Dec 2020 19:38:09 GMT

{
    "timestamp": "2020-12-26T19:38:09.426+00:00",
    "status": 404,
    "error": "Not Found",
    "message": "",
    "path": "/actor/8"
}

장점으로는

  1. 같은 예외 타입이라도 다른 상태와 다른 메세지를 지정해서 응답을 보낼 수 있다. → 결합도를 낮춘다.
  2. 불필요한 예외 타입을 추가로 만들지 않아도 된다.

예외 처리 우선 순위

ResolverMatchingPriority
ExceptionHandlerExceptionResolver@ExceptionHandler → custom exception handling1
ResponseStatusExceptionResolver@ResponseStatus → HTTP status code & ResponseStatusException2
DefaultHandlerExceptionResolverexceptions from Spring → HTTP Status code3